Skip to content

issue #340: do not apply the choice's maxOccurs bound to the chosen element's items count#345

Open
aldenw wants to merge 1 commit into
WsdlToPhp:developfrom
aldenw:feature/issue-340-choice-max-occurs
Open

issue #340: do not apply the choice's maxOccurs bound to the chosen element's items count#345
aldenw wants to merge 1 commit into
WsdlToPhp:developfrom
aldenw:feature/issue-340-choice-max-occurs

Conversation

@aldenw

@aldenw aldenw commented Jul 16, 2026

Copy link
Copy Markdown

Fixes #340

Problem

When an element inside a choice defines its own maxOccurs greater than 1, the generated setter/adder wrongly applies the choice's maxOccurs (usually the default, 1) to the element's items count:

<complexType name="ItemsChoiceType">
    <sequence>
        <choice>
            <element ref="tns:ItemIdentifier" minOccurs="1" maxOccurs="5"/>
            <element ref="tns:ItemName" minOccurs="1" maxOccurs="5"/>
        </choice>
    </sequence>
</complexType>

generates:

// validation for constraint: choiceMaxOccurs(1)
if (is_array($itemIdentifier) && count($itemIdentifier) > 1) {
    throw new InvalidArgumentException(...);
}
// validation for constraint: maxOccurs(5)
if (is_array($itemIdentifier) && count($itemIdentifier) > 5) {
    throw new InvalidArgumentException(...);
}

The first check makes the second unreachable: passing more than 1 item throws, even though the schema allows up to 5. Per XSD semantics, the choice's maxOccurs bounds how many times the choice group may repeat — not how many times the chosen element may occur within it; the element's occurrences are bounded by its own maxOccurs.

This is the same defect reported in #340 (there with maxOccurs="unbounded"). We hit it in production against Visa's VROL RTSI WSDL, whose MarkBatchQueueItemAsReadRequestType declares <choice><element ref="vsi:BatchQueueItemSID" minOccurs="1" maxOccurs="5500"/>...</choice> — the generated SDK rejects marking more than one queue item as read.

Note: the test added in #342 could not reproduce the issue because in odigeo.wsdl the choice itself carries maxOccurs="unbounded", which MaxOccursRule::testConditions() already skips. The bug appears when the choice's maxOccurs is numeric (e.g. the default 1) while the child element's own maxOccurs is greater than 1 or unbounded.

Fix

ChoiceMaxOccursRule now skips the items-count check when the element's own maxOccurs allows more than one occurrence — in that case the element's items count is already (and correctly) constrained by its own maxOccurs rule. When the element may occur at most once per group repetition, the behavior is unchanged: the choice's bound is applied as before, and the mutual-exclusion choice validation is untouched.

This required removing the final keyword from MaxOccursRule::testConditions() so the subclass can override it.

Tests

  • Added an ItemsChoiceType complex type to tests/resources/unit_tests.wsdl reproducing the shape above (element refs with minOccurs="1" maxOccurs="5" inside a default-occurrence choice) and regenerated parsed_unit_tests_{none,start}.json (additive only).
  • StructTest::testStructItemsChoiceTypeFromUnitTests asserts the generated file contains the maxOccurs(5) check and no choiceMaxOccurs(1) items-count check.
  • ChoiceMaxOccursRuleTest asserts at runtime that: several items pass (set and add), more than 5 items still throws (maxOccurs), and setting the other choice property still throws (choice constraint intact).

All new tests fail on develop without the src change. No other expected generated file changes: no existing fixture combines a numeric-maxOccurs choice with a multi-occurrence child.

… chosen element's items count

The choice's maxOccurs attribute bounds the number of times the choice group may repeat, not the number of times the chosen element may occur within the group. When the element inside the choice defines its own maxOccurs greater than 1 (or unbounded), the generated setter/adder wrongly rejected any content with more items than the choice's maxOccurs (usually 1), even though the element's own maxOccurs allows them. The element's items count is already constrained by its own maxOccurs rule, so the ChoiceMaxOccursRule now skips the count check in that case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Constraints generate after misread of choice block

1 participant